/*
 * @Description: 三维热力图
 * @Version: 1.0
 * @Author: Thomaz
 */
import * as Cesium from "cesium";
import * as h337 from "heatmap.js-fixed/build/heatmap";

class HeatmapPrimitive {
  constructor(viewer, opt) {
    if (!viewer) return;
    if (!opt.list || opt.list.length < 2) return;
    this.viewer = viewer;
    this.list = opt.list || []; // 热力值数组
    this.baseHeight = opt.baseHeight || 0; // 最低高度
    this.raduis = opt.raduis || 20; // 热力点半径
    this.gradient = opt.gradient || {
      ".1": "blue",
      ".5": "yellow",
      ".7": "red",
      ".99": "white",
    }; // 颜色配置
    this.dom = undefined;
    this.id = Number(
      new Date().getTime() + "" + Number(Math.random() * 1000).toFixed(0)
    );
    this.primitiveType = opt.primitiveType || "TRIANGLES"; //格网（LINES）和三角面（TRIANGLES）
    this.mapDom = opt.mapDom;
    this.init();
  }

  init() {
    this.canvasw = 200;
    this.bound = undefined; // 四角坐标
    this.rect = {}; // 经纬度范围
    this.x_axios = undefined; // x 轴
    this.y_axios = undefined; // y 轴
    this.girthX = 0; // x轴长度
    this.girthY = 0; // y轴长度
    this.heatmap = undefined;
    this.primitive = undefined;

    this.createHeatmap();
    this.createPrimitive();
  }

  createHeatmap() {
    this.dom = document.createElement("div");
    this.dom.id = `${this.id}`;
    this.dom.style.width = this.canvasw + "px";
    this.dom.style.height = this.canvasw + "px";
    this.dom.style.position = "absolute";
    this.dom.style.display = "none";
    let mapDom = this.mapDom;
    mapDom.appendChild(this.dom);

    let config = {
      container: document.getElementById(`${this.id}`),
      radius: this.raduis,
      maxOpacity: 0.7,
      minOpacity: 0,
      blur: 0.75,
      gradient: this.gradient,
    };
    this.heatmap = h337.create(config);
  }

  /**
   * 将经纬度坐标转换为Cesium中的Cartesian3坐标。
   * 计算所有坐标的边界，并计算每个坐标在边界内的相对位置。
   * 创建一个热力图，并向其添加数据。
   * 使用热力图作为纹理，创建一个平面几何体，并将其渲染到场景中。
   */
  createPrimitive() {
    this.hierarchy = [];
    // 遍历列表中的每个元素
    for (let ind = 0; ind < this.list.length; ind++) {
      // 将经纬度坐标从度转换为Cartesian3坐标系下的三维坐标
      let position = Cesium.Cartesian3.fromDegrees(
        this.list[ind].lnglat[0],
        this.list[ind].lnglat[1],
        0
      );
      // 将转换后的坐标添加到hierarchy数组中
      this.hierarchy.push(position);
    }
    // 计算hierarchy数组所包围的边界
    this.computeBound(this.hierarchy);
    // 创建一个空数组来存储热力图点数据
    let points = [];
    // 遍历hierarchy数组中的每个坐标
    for (let i = 0; i < this.hierarchy.length; i++) {
      // 获取当前坐标
      let p1 = this.hierarchy[i];
      // 计算当前坐标在边界内的相对位置
      const rete = this.computeRateInBound(p1);
      // 将计算出的相对位置和对应的值添加到points数组中
      points.push({
        x: rete.x,
        y: rete.y,
        value: this.list[i].value,
      });
    }
    // 向热力图添加数据
    this.heatmap.addData(points);
    // 创建一个新的GeometryInstance实例，其中包含自定义几何体
    let instance = new Cesium.GeometryInstance({
      geometry: this.createGeometry(), // 自定义几何体
    });
    // 创建一个新的Primitive实例，用来渲染几何体
    this.primitive = this.viewer.scene.primitives.add(
      new Cesium.Primitive({
        geometryInstances: instance, // 几何体实例
        appearance: new Cesium.MaterialAppearance({
          // 材质外观
          material: new Cesium.Material({
            // 定义材质
            fabric: {
              // 材质属性
              type: "Image", // 图像类型
              uniforms: {
                // 统一变量
                image: this.heatmap.getDataURL(), // 热力图数据URL
              },
            },
          }),
          translucent: true, // 设置透明度
          flat: true, // 平面模式
        }),
        asynchronous: false, // 异步加载
      })
    );
    // 为primitive设置一个标识
    this.primitive.id = "heatmap3d";
  }

  /**
   * 销毁
   */
  destroy() {
    let dom = document.getElementById(`${this.id}`);
    if (dom) dom.remove();
    if (this.primitive) {
      this.viewer.scene.primitives.remove(this.primitive);
      this.primitive = undefined;
    }
  }

  // 计算当前坐标在范围中位置 换算为canvas中的像素坐标
  computeRateInBound(position) {
    // 如果传入的位置坐标不存在，则直接返回
    if (!position) return;
    // 将位置坐标转换为地理坐标，并将高度设置为0
    let ctgc = Cesium.Cartographic.fromCartesian(position.clone());
    ctgc.height = 0;
    // 将地理坐标重新转换为笛卡尔坐标
    position = Cesium.Cartographic.toCartesian(ctgc.clone());
    // 计算位置坐标相对于左上角顶点的偏移量
    const p_origin = Cesium.Cartesian3.subtract(
      position.clone(),
      this.bound.leftTop,
      new Cesium.Cartesian3()
    );
    // 计算位置坐标沿x轴方向的偏移量
    const diffX = Cesium.Cartesian3.dot(p_origin, this.x_axios);
    // 计算位置坐标沿y轴方向的偏移量
    const diffY = Cesium.Cartesian3.dot(p_origin, this.y_axios);
    // 返回位置坐标的归一化屏幕坐标
    return {
      // 沿x轴的归一化屏幕坐标值
      x: Number((diffX / this.girthX) * this.canvasw).toFixed(0),
      // 沿y轴的归一化屏幕坐标值
      y: Number((diffY / this.girthY) * this.canvasw).toFixed(0),
    };
  }

  /**
   * 计算一个AABB包围盒的边界，包括它的四个顶点坐标以及x轴和y轴的方向单位向量。这些计算基于传入的坐标点集合，首先通过这些点计算出一个包围球，然后确定一个正方形边界，最后计算出边界的相关属性
   * @param {*} positions
   * @returns
   */
  computeBound(positions) {
    // 先转化为正方形
    // 如果没有传入坐标点，则直接返回
    if (!positions) return;
    // 从给定的坐标点集合计算出一个包围球
    let boundingSphere = Cesium.BoundingSphere.fromPoints(
      positions,
      new Cesium.BoundingSphere()
    );
    console.log("包围球boundingSphere", boundingSphere);
    // 获取包围球的中心点
    let center = boundingSphere.center;
    // 获取包围球的半径
    const radius = boundingSphere.radius;
    // 从中心点坐标构建一个东-北-上坐标系到固定坐标系的转换矩阵
    let modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(center.clone());
    // 计算该矩阵的逆矩阵
    let modelMatrix_inverse = Cesium.Matrix4.inverse(
      modelMatrix.clone(),
      new Cesium.Matrix4()
    );
    // 定义一个沿着y轴的单位向量
    let roate_y = new Cesium.Cartesian3(0, 1, 0);
    // 初始化一个数组用于存储四个角点
    let rect = [];
    // 循环计算四个角点坐标
    for (let i = 45; i <= 360; i += 90) {
      // 计算旋转角度对应的旋转矩阵
      let roateZ_mtx = Cesium.Matrix3.fromRotationZ(
        Cesium.Math.toRadians(i),
        new Cesium.Matrix3()
      );
      // 计算旋转后的y轴单位向量
      let yaix_roate = Cesium.Matrix3.multiplyByVector(
        roateZ_mtx,
        roate_y,
        new Cesium.Cartesian3()
      );
      // 归一化得到单位长度的向量
      yaix_roate = Cesium.Cartesian3.normalize(
        yaix_roate,
        new Cesium.Cartesian3()
      );
      // 将单位向量乘以包围球的半径
      let third = Cesium.Cartesian3.multiplyByScalar(
        yaix_roate,
        radius,
        new Cesium.Cartesian3()
      );
      // 通过模型矩阵将该向量转换回世界坐标系
      let poi = Cesium.Matrix4.multiplyByPoint(
        modelMatrix,
        third.clone(),
        new Cesium.Cartesian3()
      );
      // 将计算出的坐标点添加到数组中
      rect.push(poi);
    }
    // 将计算出的坐标点转换为经纬度坐标
    let lnglats = this.cartesiansToLnglats(rect, this.viewer);
    // 初始化最小纬度和最大纬度以及最小经度和最大经度
    let minLat = Number.MAX_VALUE,
      maxLat = Number.MIN_VALUE,
      minLng = Number.MAX_VALUE,
      maxLng = Number.MIN_VALUE;
    // 遍历所有坐标点
    const length = rect.length;
    for (let i = 0; i < length; i++) {
      const lnglat = lnglats[i];
      // 更新最小经度
      if (lnglat[0] < minLng) {
        minLng = lnglat[0];
      }
      // 更新最大经度
      if (lnglat[0] > maxLng) {
        maxLng = lnglat[0];
      }

      // 更新最小纬度
      if (lnglat[1] < minLat) {
        minLat = lnglat[1];
      }
      // 更新最大纬度
      if (lnglat[1] > maxLat) {
        maxLat = lnglat[1];
      }
    }
    // 计算纬度差和经度差
    const diff_lat = maxLat - minLat;
    const diff_lng = maxLng - minLng;
    // 放大正方形轮廓
    this.rect.minLat = minLat - diff_lat / length;
    this.rect.maxLat = maxLat + diff_lat / length;
    this.rect.minLng = minLng - diff_lng / length;
    this.rect.maxLng = maxLng + diff_lng / length;
    // 定义矩形的四个顶点
    this.bound = {
      leftTop: Cesium.Cartesian3.fromDegrees(
        this.rect.minLng,
        this.rect.maxLat
      ),
      leftBottom: Cesium.Cartesian3.fromDegrees(
        this.rect.minLng,
        this.rect.minLat
      ),
      rightTop: Cesium.Cartesian3.fromDegrees(
        this.rect.maxLng,
        this.rect.maxLat
      ),
      rightBottom: Cesium.Cartesian3.fromDegrees(
        this.rect.maxLng,
        this.rect.minLat
      ),
    };
    // 绘制AABB包围盒
    this.viewer.entities.add({
      name: "AABB包围盒",
      polygon: {
        hierarchy: Cesium.Cartesian3.fromDegreesArray([
          this.rect.minLng,
          this.rect.minLat,
          this.rect.minLng,
          this.rect.maxLat,
          this.rect.maxLng,
          this.rect.maxLat,
          this.rect.maxLng,
          this.rect.minLat,
        ]),
        material: Cesium.Color.YELLOW.withAlpha(0.5),
      },
    });
    // 计算boundingRectangle
    const boundingRectangle = Cesium.BoundingRectangle.fromPoints(
      positions,
      new Cesium.BoundingSphere()
    );
    console.log("boundingRectangle", boundingRectangle);
    this.viewer.entities.add({
      name: "包围球",
      position: boundingSphere.center, // 点的经纬度坐标
      ellipsoid: {
        radii: new Cesium.Cartesian3(boundingSphere.radius, boundingSphere.radius, boundingSphere.radius), //xyz三个方向的半径，相等则为球
        material: Cesium.Color.GREEN.withAlpha(0.3), // 材质颜色和透明度
      },
    });
    console.log("定义矩形的四个顶点this.bound", this.bound);
    // 计算x轴方向的单位向量
    this.x_axios = Cesium.Cartesian3.subtract(
      this.bound.rightTop,
      this.bound.leftTop,
      new Cesium.Cartesian3()
    );
    this.x_axios = Cesium.Cartesian3.normalize(
      this.x_axios,
      new Cesium.Cartesian3()
    );
    // 计算y轴方向的单位向量
    this.y_axios = Cesium.Cartesian3.subtract(
      this.bound.leftBottom,
      this.bound.leftTop,
      new Cesium.Cartesian3()
    );
    this.y_axios = Cesium.Cartesian3.normalize(
      this.y_axios,
      new Cesium.Cartesian3()
    );
    // 计算矩形的宽度
    this.girthX = Cesium.Cartesian3.distance(
      this.bound.rightTop,
      this.bound.leftTop
    );
    // 计算矩形的高度
    this.girthY = Cesium.Cartesian3.distance(
      this.bound.leftBottom,
      this.bound.leftTop
    );
    console.log("x轴方向的单位向量", this.x_axios);
    console.log("y轴方向的单位向量", this.y_axios);
    console.log("矩形的宽度", this.girthX);
    console.log("矩形的高度", this.girthY);
  }

  createGeometry() {
    // 调用getGrain方法获取顶点数据
    let opt = this.getGrain();
    console.log("自定义Geometry的自定义数据", opt);
    // 创建一个新的Cesium几何体对象
    let geometry = new Cesium.Geometry({
      // 定义几何体的属性
      attributes: new Cesium.GeometryAttributes({
        // 定义位置属性
        position: new Cesium.GeometryAttribute({
          // 设置每个顶点的组件数据类型为双精度浮点数
          componentDatatype: Cesium.ComponentDatatype.DOUBLE,
          // 每个顶点有三个分量（x, y, z）
          componentsPerAttribute: 3,
          // 顶点位置数据
          values: opt.positions,
        }),
        // 定义纹理坐标属性
        st: new Cesium.GeometryAttribute({
          // 设置每个纹理坐标的组件数据类型为单精度浮点数
          componentDatatype: Cesium.ComponentDatatype.FLOAT,
          // 每个纹理坐标有两个分量（s, t）
          componentsPerAttribute: 2,
          // 纹理坐标数据
          values: new Float32Array(opt.st),
        }),
      }),
      // 定义索引数组
      indices: new Uint16Array(opt.indices),
      // 定义基本图元类型
      primitiveType: Cesium.PrimitiveType[this.primitiveType],
      // 定义包围球
      boundingSphere: Cesium.BoundingSphere.fromVertices(opt.positions),
    });
    // 返回创建好的几何体
    return geometry;
  }

  // 根据经纬度跨度和canvas的宽高来计算顶点坐标及纹理坐标
  getGrain(opt) {
    // 获取canvas宽度
    let canvasW = this.canvasW || 200;
    // 获取canvas高度
    let canvasH = this.canvasH || 200;
    // 获取最大经度
    let maxLng = this.rect.maxLng;
    // 获取最大纬度
    let maxLat = this.rect.maxLat;
    // 获取最小经度
    let minLng = this.rect.minLng;
    // 获取最小纬度
    let minLat = this.rect.minLat;
    // 计算经度粒度
    const granLng_w = (maxLng - minLng) / canvasW;
    // 计算纬度粒度
    const granLat_H = (maxLat - minLat) / canvasH;
    // 初始化顶点位置数组
    let positions = [];
    // 初始化纹理坐标数组
    let st = [];
    // 初始化索引数组
    let indices = [];
    // 用于存储顶点坐标
    let points = [];
    // 循环遍历canvas宽度
    for (let i = 0; i < canvasW; i++) {
      // 计算当前经度
      let nowLng = minLng + granLng_w * i;

      // 循环遍历canvas高度
      for (let j = 0; j < canvasH; j++) {
        // 计算当前纬度
        let nowLat = minLat + granLat_H * j;
        // 获取当前位置的热力值
        const value = this.heatmap.getValueAt({
          x: i,
          y: j,
        });
        // 将当前经纬度转换为三维笛卡尔坐标，并加上基础高度和热力值作为高度
        let cartesian3 = Cesium.Cartesian3.fromDegrees(
          nowLng,
          nowLat,
          this.baseHeight + value
        );
        // 添加顶点位置到positions数组
        positions.push(cartesian3.x, cartesian3.y, cartesian3.z);
        // 添加纹理坐标到st数组
        st.push(i / canvasW, j / canvasH);
        // 如果不是最后一行或最后一列，则添加索引
        if (j !== canvasH - 1 && i !== canvasW - 1) {
          // 添加两个三角形的索引
          indices.push(
            i * canvasH + j,
            i * canvasH + j + 1,
            (i + 1) * canvasH + j
          );
          indices.push(
            (i + 1) * canvasH + j,
            (i + 1) * canvasH + j + 1,
            i * canvasH + j + 1
          );
        }
      }
    }
    // 返回顶点数据
    return {
      positions: positions,
      st: st,
      indices: indices,
    };
  }

  /**
   * 世界坐标数组转经纬度数组
   * @param {Cesium.Cartesian3[]} cartesians 世界坐标数组
   * @param {Cesium.Viewer} viewer 当前viewer对象
   * @returns { Array } 经纬度坐标数组
   */
  cartesiansToLnglats(cartesians, viewer) {
    if (!cartesians || cartesians.length < 1) return;
    viewer = viewer || window.viewer;
    if (!viewer) {
      console.log("util.cartesiansToLnglats方法缺少viewer对象");
      return;
    }
    var arr = [];
    for (var i = 0; i < cartesians.length; i++) {
      arr.push(this.cartesianToLnglat(cartesians[i], viewer));
    }
    return arr;
  }

  /**
   * 世界坐标转经纬度
   * @param {Cesium.Cartesian3 } cartesian 世界坐标
   * @param {Cesium.Viewer} viewer 当前viewer对象
   * @returns { Array } 经纬度坐标s
   */
  cartesianToLnglat(cartesian, viewer) {
    if (!cartesian) return [];
    viewer = viewer || window.viewer;
    var lnglat = Cesium.Cartographic.fromCartesian(cartesian);
    var lat = Cesium.Math.toDegrees(lnglat.latitude);
    var lng = Cesium.Math.toDegrees(lnglat.longitude);
    var hei = lnglat.height;
    return [lng, lat, hei];
  }
}

export default HeatmapPrimitive;
